package org.erikaredmark.monkeyshines.menu; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.JPanel; import org.erikaredmark.monkeyshines.World; import org.erikaredmark.monkeyshines.encoder.EncodedWorld; import org.erikaredmark.monkeyshines.encoder.WorldIO; import org.erikaredmark.monkeyshines.encoder.exception.WorldRestoreException; import org.erikaredmark.monkeyshines.graphics.exception.ResourcePackException; import org.erikaredmark.monkeyshines.resource.WorldResource; import org.erikaredmark.monkeyshines.resource.WorldResource.UseIntent; import org.erikaredmark.util.BinaryLocation; /** * * Primary panel that allows the user to select from either a builtin world or a custom one. * * @author Erika Redmark * */ public final class SelectAWorld extends JPanel { private static final long serialVersionUID = 1L; private static final String CLASS_NAME = "org.erikaredmark.monkeyshines.menu.SelectAWorld"; private static final Logger LOGGER = Logger.getLogger(CLASS_NAME); private static final int BIG_BUTTON_WIDTH = 104; private static final int BIG_BUTTON_HEIGHT = 79; private static final int NON_OTHER_Y = 34; private static final int SPOOKED_X = 34; private static final int SPACED_OUT_X = 151; private static final int ABOUT_THE_HOUSE_X = 268; private static final int IN_THE_DRINK_X = 385; private static final int IN_THE_SWING_X = 502; private static final int OTHER_X = 385; private static final int OTHER_Y = 128; private BufferedImage background; // loading of all built in world button images private BufferedImage spookedDown; private BufferedImage spookedUp; private BufferedImage spacedDown; private BufferedImage spacedUp; private BufferedImage aboutTheHouseDown; private BufferedImage aboutTheHouseUp; private BufferedImage inTheDrinkDown; private BufferedImage inTheDrinkUp; private BufferedImage inTheSwingDown; private BufferedImage inTheSwingUp; private BufferedImage otherUp; private BufferedImage otherDown; /** * * Creates the select world panel, with a callback that is fired whenever the user uses the panel to select a world. It is up to the client * how to position this panel and control the lifetime of the panel. * * @param callback * */ public SelectAWorld(final WorldSelectionCallback callback) { try { background = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/selectWorldBackground.png") ); spookedDown = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/SpookedDown.png") ); spookedUp = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/SpookedUp.png") ); spacedDown = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/SpacedOutDown.png") ); spacedUp = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/SpacedOutUp.png") ); aboutTheHouseDown = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/AboutTheHouseDown.png") ); aboutTheHouseUp = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/AboutTheHouseUp.png") ); inTheDrinkDown = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/InTheDrinkDown.png") ); inTheDrinkUp = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/InTheDrinkUp.png") ); inTheSwingDown = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/InTheSwingDown.png") ); inTheSwingUp = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/InTheSwingUp.png") ); otherDown = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/OtherDown.png") ); otherUp = ImageIO.read(SelectAWorld.class.getResourceAsStream("/resources/graphics/mainmenu/selectworld/OtherUp.png") ); } catch (IOException e) { throw new RuntimeException("Failed to load resources expected in .jar: " + e.getMessage(), e); } setLayout(null); JButton spookedButton = new BigWorldButton(spookedUp, spookedDown); spookedButton.setLocation(SPOOKED_X, NON_OTHER_Y); spookedButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { World world = loadInternalWorld(SelectAWorld.this, InternalWorld.SPOOKED); if (world != null) { callback.worldSelected(world); } } }); add(spookedButton); JButton spacedButton = new BigWorldButton(spacedUp, spacedDown); spacedButton.setLocation(SPACED_OUT_X, NON_OTHER_Y); spacedButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { World world = loadInternalWorld(SelectAWorld.this, InternalWorld.SPACED_OUT); if (world != null) { callback.worldSelected(world); } } }); add(spacedButton); JButton aboutTheHouseButton = new BigWorldButton(aboutTheHouseUp, aboutTheHouseDown); aboutTheHouseButton.setLocation(ABOUT_THE_HOUSE_X, NON_OTHER_Y); aboutTheHouseButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { World world = loadInternalWorld(SelectAWorld.this, InternalWorld.ABOUT_THE_HOUSE); if (world != null) { callback.worldSelected(world); } } }); add(aboutTheHouseButton); JButton inTheDrinkButton = new BigWorldButton(inTheDrinkUp, inTheDrinkDown); inTheDrinkButton.setLocation(IN_THE_DRINK_X, NON_OTHER_Y); inTheDrinkButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { World world = loadInternalWorld(SelectAWorld.this, InternalWorld.IN_THE_DRINK); if (world != null) { callback.worldSelected(world); } } }); add(inTheDrinkButton); JButton inTheSwingButton = new BigWorldButton(inTheSwingUp, inTheSwingDown); inTheSwingButton.setLocation(IN_THE_SWING_X, NON_OTHER_Y); inTheSwingButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { World world = loadInternalWorld(SelectAWorld.this, InternalWorld.IN_THE_SWING); if (world != null) { callback.worldSelected(world); } } }); add(inTheSwingButton); JButton otherButton = new BigWorldButton(otherUp, otherDown); otherButton.setLocation(OTHER_X, OTHER_Y); otherButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { World world = loadCustomWorld(SelectAWorld.this); if (world != null) { callback.worldSelected(world); } } }); add(otherButton); setSize(640, 480); setMinimumSize(new Dimension(640, 480) ); setPreferredSize(new Dimension(640, 480) ); } /** * * Loads an internal world in this .jar. If this fails, the .jar is bad, and a dialog will appear * with the exception info and an exception stacktrace will be logged. Otherwise, loads the level * from the .jar with the given name. * * @param name * @return */ private static World loadInternalWorld(Component parent, InternalWorld chosenWorld) { // Can skip WorldIO and just jump to Encoded since we have a stream. try (InputStream is = SelectAWorld.class.getResourceAsStream(chosenWorld.internalPath); InputStream rsrcIs = SelectAWorld.class.getResourceAsStream(chosenWorld.internalResourcePath) ) { EncodedWorld world = EncodedWorld.fromStream(is); // Bit of a hack, since right now a valid File object is needed to use the entire resource loading // code which uses the ZipFile class. Extract .jar'ed zip into temporary filesystem. Path tempRsrcDir = Files.createTempDirectory("monkeyshines_temp_resources"); Path tempRsrc = tempRsrcDir.resolve("rsrc.zip"); Files.copy(rsrcIs, tempRsrc); WorldResource rsrc = WorldResource.fromPack(tempRsrc, UseIntent.GAME); // Clean up temporary files try { Files.delete(tempRsrc); Files.delete(tempRsrcDir); } catch (IOException e) { LOGGER.log(Level.WARNING, CLASS_NAME + ": Could not delete temporary files (should not affect gameplay) due to: " + e.getMessage(), e); } return world.newWorldInstance(rsrc); } catch (Exception e) { LOGGER.severe(CLASS_NAME + ": Missing world " + chosenWorld.internalPath + " from .jar file. Possible .jar corruption."); handleWorldLoadException(parent, e); } return null; } /** * * Attempts to load a world by giving the user a file chooser. If a world is loaded, bonzo is set and * gameplay begins proper. Otherwise, state does not transition to playing and user remains back on main * menu. * <p/> * The world will be fully constructed and set up when returned and can be passed directly to a * {@code GameWindow} to be played. * * @param parent * the parent component for displaying the dialog on * * @return * the selected world, or {@code null} if no world was selected. * */ public static World loadCustomWorld(final Component parent) { JFileChooser fileChooser = new JFileChooser(); fileChooser.setCurrentDirectory(BinaryLocation.BINARY_LOCATION.toFile() ); System.out.println(fileChooser.getCurrentDirectory() ); if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { Path worldFile = fileChooser.getSelectedFile().toPath(); try { EncodedWorld world = WorldIO.restoreWorld(worldFile); // Try to load the resource pack String fileName = worldFile.getFileName().toString(); // Remove .world extension so we can substitute with .zip. String worldName = fileName.substring(0, fileName.lastIndexOf('.') ); Path packFile = worldFile.getParent().resolve(worldName + ".zip"); WorldResource rsrc = WorldResource.fromPack(packFile, UseIntent.GAME); return world.newWorldInstance(rsrc); } catch (Exception e) { // See method. Instances in if/else are the exception we expect to catch. // This technically catches everything but I see no reason why any exceptions // need propogate any further since handling means logging anyway. handleWorldLoadException(parent, e); } } // no world chosen if method hasn't returned yet return null; } private static void handleWorldLoadException(final Component parent, Exception ex) { if (ex instanceof WorldRestoreException) { JOptionPane.showMessageDialog(parent, "Cannot load world: Possibly corrupt or not a world file: " + ex.getMessage(), "Loading Error", JOptionPane.ERROR_MESSAGE); LOGGER.log(Level.WARNING, CLASS_NAME + ": Cannot load world: Possibly corrupt or not a world file: " + ex.getMessage(), ex); } else if (ex instanceof ResourcePackException) { JOptionPane.showMessageDialog(parent, "Resource pack issues: " + ex.getMessage(), "Loading Error", JOptionPane.ERROR_MESSAGE); LOGGER.log(Level.WARNING, CLASS_NAME + ": Cannot load world: Possibly corrupt or not a world file: " + ex.getMessage(), ex); } else if (ex instanceof IOException) { JOptionPane.showMessageDialog(parent, "Low level I/O error: " + ex.getMessage(), "Loading Error", JOptionPane.ERROR_MESSAGE); LOGGER.log(Level.WARNING, CLASS_NAME + ": " + ex.getMessage(), ex); } else { JOptionPane.showMessageDialog(parent, "Unknown Error loading world " + ex.getMessage(), "Loading Error", JOptionPane.ERROR_MESSAGE); LOGGER.log(Level.WARNING, CLASS_NAME + ": " + ex.getMessage(), ex); } } /** * * Paint the standard button components for clicking, but everything else can easily just be painted on since they * aren't interactive. * */ @Override public void paintComponent(Graphics g) { g.drawImage(background, 0, 0, null); // Do not call; destroys background. Components are still painted regardless. // super.paintComponent(g); } /** * * The big buttons that appear allowing the user to select a world. All buttons are the same size and * contain an image that will be shrunk and applied a special bevel effect. Some will load up a specific * world and the 'Other' button will allow a JChooser to pick a custom level to load. * <p/> * All buttons are supplied a special callback object that, when clicked and finished processing the click, * may call with a valid World object assuming they were able to get a hold of one. * */ private static final class BigWorldButton extends JButton { private static final long serialVersionUID = 1L; /** * * Constructs the button with the appropriate pressed and unpressed images. * * @param up * image shown when button is not clicked * * @param down * image shown when button is clicked * */ public BigWorldButton(BufferedImage up, BufferedImage down) { super(); setIcon(new ImageIcon(up) ); setSize(BIG_BUTTON_WIDTH, BIG_BUTTON_HEIGHT); setPressedIcon(new ImageIcon(down) ); //addActionListener(listener); MenuUtils.renderImageOnly(this); } } private enum InternalWorld { SPOOKED("/resources/worlds/Spooked/Spooked.world", "/resources/worlds/Spooked/Spooked.zip"), SPACED_OUT("/resources/worlds/SpacedOut/Spaced Out.world", "/resources/worlds/SpacedOut/Spaced Out.zip"), ABOUT_THE_HOUSE("/resources/worlds/AboutTheHouse/About The House.world", "/resources/worlds/AboutTheHouse/About The House.zip"), IN_THE_DRINK("/resources/worlds/InTheDrink/In The Drink.world", "/resources/worlds/InTheDrink/In The Drink.zip"), IN_THE_SWING("/resources/worlds/In The Swing/In The Swing.world", "/resources/worlds/In The Swing/In The Swing.zip"); public final String internalPath; public final String internalResourcePath; private InternalWorld(final String path, final String resourcePath) { internalPath = path; internalResourcePath = resourcePath; } } /** * * Effectively a runnable that is called with a World reference. Used to communicate back to the client * creating this panel the selected world. Typically this being called, the client will close or otherwise * dispose of the panel. * * @author Erika Redmark * */ public interface WorldSelectionCallback { void worldSelected(World world); } }